package com.j2eefast.compressor.maven;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.crypto.digest.MD5;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import java.io.*;
import java.util.*;
import java.util.zip.GZIPOutputStream;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Parameter;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import com.yahoo.platform.yui.compressor.CssCompressor;
import com.yahoo.platform.yui.compressor.JavaScriptCompressor;


/**
 * Apply compression on JS and CSS (using YUI Compressor).
 * 优化压缩逻辑,未改变使用之前压缩的文件,不再压缩提高效率
 * @goal compress
 * @phase process-resources
 *
 * @author David Bernard
 * @author J2eeFAST
 * @created 2007-08-28
 * @threadSafe
 */
@Mojo(name = "fast-compress", defaultPhase = LifecyclePhase.PROCESS_RESOURCES, threadSafe = false)
public class FastCompressorMojo  extends MojoSupport {

    /**
     * Read the input file using "encoding".
     *
     * @ parameter property="file.encoding" default-value="UTF-8"
     */
    @Parameter(property = "encoding", defaultValue = "UTF-8")
    private String encoding;

    /**
     * The output filename suffix.
     *
     * @ parameter property="maven.compressor.suffix" default-value="-min"
     */
    @Parameter(property = "suffix", defaultValue = "-min")
    private String suffix;

    /**
     * If no "suffix" must be add to output filename (maven's configuration manage empty suffix like default).
     *
     * @ parameter property="maven.compressor.nosuffix" default-value="false"
     */
    @Parameter(property = "nosuffix", defaultValue = "false")
    private boolean nosuffix;

    /**
     * Insert line breaks in output after the specified column number.
     *
     * @ parameter property="maven.compressor.linebreakpos" default-value="-1"
     */
    @Parameter(property = "linebreakpos", defaultValue = "-1")
    private int linebreakpos;

    /**
     * [js only] No compression
     *
     * @ parameter property="maven.compressor.nocompress" default-value="false"
     */
    @Parameter(property = "nocompress", defaultValue = "false")
    private boolean nocompress;

    /**
     * [js only] Minify only, do not obfuscate.
     *
     * @ parameter property="maven.compressor.nomunge" default-value="false"
     */
    @Parameter(property = "nomunge", defaultValue = "false")
    private boolean nomunge;

    /**
     * [js only] Preserve unnecessary semicolons.
     *
     * @ parameter property="maven.compressor.preserveAllSemiColons" default-value="false"
     */
    @Parameter(property = "preserveAllSemiColons", defaultValue = "false")
    private boolean preserveAllSemiColons;

    /**
     * [js only] disable all micro optimizations.
     *
     * @ parameter property="maven.compressor.disableOptimizations" default-value="false"
     */
    @Parameter(property = "disableOptimizations", defaultValue = "false")
    private boolean disableOptimizations;

    /**
     * force the compression of every files, else if compressed file already exists and is younger than source file, nothing is done.
     *
     * @ parameter property="maven.compressor.force" default-value="false"
     */
    @Parameter(property = "force", defaultValue = "false")
    private boolean force;

    /**
     * a list of aggregation/concatenation to do after processing, for example to create big js files that contain several small js files. Aggregation
     * could be done on any type of file (js, css, ...).
     *
     * @ parameter
     */
    @Parameter(property = "aggregations")
    private Aggregation[] aggregations;

    /**
     * request to create a gzipped version of the yuicompressed/aggregation files.
     *
     * @ parameter property="maven.compressor.gzip" default-value="false"
     */
    @Parameter(property = "gzip", defaultValue = "false")
    private boolean gzip;

    /**
     * show statistics (compression ratio).
     *
     * @ parameter property="maven.compressor.statistics" default-value="true"
     */
    @Parameter(property = "statistics", defaultValue = "true")
    private boolean statistics;

    /**
     * aggregate files before minify
     *
     * @ parameter property="maven.compressor.preProcessAggregates" default-value="false"
     */
    @Parameter(property = "preProcessAggregates", defaultValue = "false")
    private boolean preProcessAggregates;

    /**
     * use the input file as output when the compressed file is larger than the original
     *
     * @ parameter property="maven.compressor.useSmallestFile"
     * default-value="false"
     */
    @Parameter(property = "useSmallestFile", defaultValue = "false")
    private boolean useSmallestFile;

    /**
     * Specifies the compilation level to use. --compilation_level
     *
     * Options: BUNDLE, WHITESPACE_ONLY, SIMPLE (default), ADVANCED
     */
    @Parameter(property = "compilationLevel", defaultValue = "SIMPLE")
    private String compilationLevel;

    @Parameter(property = "googleGcPath", defaultValue = "/lib")
    private String googleGcPath;

    private long inSizeTotal_;
    private long outSizeTotal_;
    /**
     * 未改动
     */
    private long nochangTotal;

    /**
     * Keep track of updated files for aggregation on incremental builds
     */
    private Set<String> incrementalFiles = null;

    @Override
    protected String[] getDefaultIncludes() throws Exception {
        return new String[]{"**/*.css", "**/*.js"};
    }

    @Override
    public void beforeProcess() throws Exception {


        getLog().info(String.format("----------------/// FAST-COMPRESSOR(JS/CSS) /// ----------------"));
        
        // 开始计时
        this.startTime = System.currentTimeMillis();

        nochangTotal = 0;
        
        if (nosuffix) {
            suffix = "";
        }

        if(preProcessAggregates) {
            aggregate();
        }
    }

    @Override
    protected void afterProcess() throws Exception {
        if (statistics && (inSizeTotal_ > 0)) {
            getLog().info(String.format("total input (%db) -> output (%db)[%d%%]  > nochage [%s]", inSizeTotal_, outSizeTotal_, ((outSizeTotal_ * 100)/inSizeTotal_), nochangTotal));
        }

        if(!preProcessAggregates) {
            aggregate();
        }

        if(uncompressedChange) {
        	FileUtil.writeBytes(this.fileKey.toString().getBytes(), this.tempDirs);
        }
        
        this.endTime = System.currentTimeMillis();
    }

    private void aggregate() throws Exception {
        if (aggregations != null) {
            Set<File> previouslyIncludedFiles = new HashSet<>();
            for(Aggregation aggregation : aggregations) {
                getLog().info("generate aggregation : " + aggregation.output);
                Collection<File> aggregatedFiles = aggregation.run(previouslyIncludedFiles,buildContext, incrementalFiles);
                previouslyIncludedFiles.addAll(aggregatedFiles);

                File gzipped = gzipIfRequested(aggregation.output);
                if (statistics) {
                    if (gzipped != null) {
                        getLog().info(String.format("%s (%db) -> %s (%db)[%d%%]", aggregation.output.getName(), aggregation.output.length(), gzipped.getName(), gzipped.length(), ratioOfSize(aggregation.output, gzipped)));
                    } else if (aggregation.output.exists()){
                        getLog().info(String.format("%s (%db)", aggregation.output.getName(), aggregation.output.length()));
                    } else {
                        getLog().warn(String.format("%s not created", aggregation.output.getName()));
                    }
                }
            }
        }
    }

    @Override
    protected void processFile(SourceFile src) throws Exception {
        File inFile = src.toFile();
        getLog().debug("on incremental build only compress if input file has Delta");
        if(buildContext.isIncremental()){
            if(!buildContext.hasDelta(inFile)){
                if (getLog().isInfoEnabled()) {
                    getLog().info("nothing to do, " + inFile + " has no Delta");
                }
                return;
            }
            if(incrementalFiles == null){
                incrementalFiles = new HashSet<String>();
            }
        }

        if (getLog().isDebugEnabled()) {
            getLog().debug("compress file :" + src.toFile()+ " to " + src.toDestFile(suffix));
        }

        File outFile = src.toDestFile(suffix);

        getLog().debug("only compress if input file is younger than existing output file");
        if (!force && outFile.exists() && (outFile.lastModified() > inFile.lastModified())) {
            if (getLog().isInfoEnabled()) {
                getLog().info("nothing to do, " + outFile + " is younger than original, use 'force' option or clean your target");
            }
            return;
        }

        //处理文件
        long inSize = 0;
        if (statistics) {
            inSize = inFile.length();
            inSizeTotal_ += inSize;
        }

        String fileMd5 = MD5.create().digestHex(outFile.getAbsolutePath());
        
        String tempName = fileMd5 + FileUtil.getName(outFile)+"."+FileUtil.getSuffix(outFile);
        
        File outFileTmp = FileUtil.touch(this.tempSourceDirectory + tempName);
        
        //inFile
    	Date inDate = FileUtil.lastModifiedTime(inFile);
        
        if(uncompressedChange && fileKey.containsKey(fileMd5)) {
        	
        	Long data = (Long) fileKey.get(fileMd5);
        	
        	if(DateUtil.isSameTime(DateUtil.date(data), inDate)) {
        		
        		nochangTotal++;
        		
        		//文件没改 直接使用之前压缩过
        		getLog().info(String.format("No changes to copy directly [%s]",FileUtil.getName(outFile)));
        		
        		boolean outputIgnored = useSmallestFile && inFile.length() < outFile.length();
        		
                if (outputIgnored) {
                    FileUtils.copyFile(inFile, outFile);
                    getLog().debug("output greater than input, using original instead");
                } else {
                    FileUtils.forceDelete(outFile);
                    FileUtils.copyFile(outFileTmp, outFile);
                    buildContext.refresh(outFile);
                }

                if(buildContext.isIncremental()){
                    incrementalFiles.add(outFile.getAbsolutePath());
                }
                
                gzipIfRequested(outFile);
                if (statistics) {
                    inSizeTotal_ += inFile.length();
                    outSizeTotal_ += outFile.length();
                }
        		return;
        	}
        }
        
        //保存最后更新时间
        if(uncompressedChange) fileKey.set(fileMd5, inDate.getTime());
        
        //File outFileTmp = new File(outFile.getAbsolutePath() + ".tmp");
        FileUtils.forceDelete(outFileTmp);

        // js压缩使用 googleCompress
        if (".js".equalsIgnoreCase(src.getExtension())) {

            // 混淆压缩
            googleCompress(compilationLevel, inFile, outFileTmp);

            if (inFile.length() < outFileTmp.length()) {
                // 失败的话 简单压缩
                long outFileTmpOrig = outFileTmp.length();
                googleCompress("WHITESPACE_ONLY", inFile, outFileTmp);
                getLog().info(String.format("%s (%db) -> %s (%db orig:%db)[whitespace only mode]", inFile.getName(),
                        inSize, outFile.getName(), outFileTmp.length(), outFileTmpOrig));
            }
        } else {
            yuiCompress(src, inFile, outFile, outFileTmp);
        }

        boolean outputIgnored = useSmallestFile && inFile.length() < outFile.length();
        if (outputIgnored) {
            //FileUtils.forceDelete(outFileTmp);
            FileUtils.copyFile(inFile, outFile);
            getLog().debug("output greater than input, using original instead");
        } else {
            FileUtils.forceDelete(outFile);
            //FileUtils.rename(outFileTmp, outFile);
            FileUtils.copyFile(outFileTmp, outFile);
            buildContext.refresh(outFile);
        }
        
        if(!uncompressedChange) FileUtils.forceDelete(outFileTmp);

        if(buildContext.isIncremental()){
            incrementalFiles.add(outFile.getAbsolutePath());
        }

        File gzipped = gzipIfRequested(outFile);
        if (statistics) {
            inSizeTotal_ += inFile.length();
            outSizeTotal_ += outFile.length();

            String fileStatistics;
            if (outputIgnored) {
                fileStatistics = String.format("%s (%db) -> %s (%db)[compressed output discarded (exceeded input size)]", inFile.getName(), inFile.length(), outFile.getName(), outFile.length());
            } else {
                fileStatistics = String.format("%s (%db) -> %s (%db)[%d%%]", inFile.getName(), inFile.length(), outFile.getName(), outFile.length(), ratioOfSize(inFile, outFile));
            }

            if (gzipped != null) {
                fileStatistics = fileStatistics + String.format(" -> %s (%db)[%d%%]", gzipped.getName(), gzipped.length(), ratioOfSize(inFile, gzipped));
            }
            getLog().info(fileStatistics);
        }
    }

    /**
     * YUI Compressor css样式压缩
     * @param in
     * @param out
     * @throws IOException
     */
    private void compressCss(InputStreamReader in, OutputStreamWriter out)
            throws IOException {
        try{
            CssCompressor compressor = new CssCompressor(in);
            compressor.compress(out, linebreakpos);
        }catch(IllegalArgumentException e){
            throw new IllegalArgumentException(
                    "Unexpected characters found in CSS file. Ensure that the CSS file does not contain '$', and try again",e);
        }
    }

//    /**
//     * Google Closure Compiler
//     * @param compilationLevel
//     * @param inFile
//     * @param outFileTmp
//     * @throws Exception
//     */
//    private void googleCompressLocl(String compilationLevel, File inFile, File outFileTmp) throws Exception {
//        List<String> args = new ArrayList<>();
//        args.add("--compilation_level=" + compilationLevel);
//        if (!jswarn) {
//            args.add("--warning_level=QUIET");
//        }
//        args.add("--js=" + inFile.getAbsolutePath());
//        args.add("--js_output_file=" + outFileTmp.getAbsolutePath());
//        getLog().info(StrUtil.join(" ",args));
//        List<String> infos = RuntimeUtil.execForLines("java -jar "+googleGcPath+" "+StrUtil.join(" ",args));
//        getLog().info(StrUtil.join("",infos));
//    }

    private void googleCompress(String compilationLevel, File inFile, File outFileTmp) throws Exception {
        List<String> args = new ArrayList<>();
        args.add("--compilation_level=" + compilationLevel);
        if (!jswarn) {
            args.add("--warning_level=QUIET");
        }
        args.add("--js=" + inFile.getAbsolutePath());
        args.add("--js_output_file=" + outFileTmp.getAbsolutePath());
        GoogleCompiler.run(args, getLog());
    }

    private void yuiCompress(SourceFile src, File inFile, File outFile, File outFileTmp) throws Exception {
        InputStreamReader in = null;
        OutputStreamWriter out = null;
        try {
            in = new InputStreamReader(new FileInputStream(inFile), encoding);
            if (!outFile.getParentFile().exists() && !outFile.getParentFile().mkdirs()) {
                throw new MojoExecutionException("Cannot create resource output directory: " + outFile.getParentFile());
            }
            getLog().debug("use a temporary outputfile (in case in == out)");

            getLog().debug("start compression");
            out = new OutputStreamWriter(buildContext.newFileOutputStream(outFileTmp), encoding);
            if (nocompress) {
                getLog().info("No compression is enabled");
                IOUtil.copy(in, out);
            } if (".js".equalsIgnoreCase(src.getExtension())) {
                JavaScriptCompressor compressor = new JavaScriptCompressor(in, jsErrorReporter_);
                compressor.compress(out, linebreakpos, !nomunge, jswarn, preserveAllSemiColons, disableOptimizations);
            } else if (".css".equalsIgnoreCase(src.getExtension())) {
                compressCss(in, out);
            }
            getLog().debug("end compression");
        } finally {
            IOUtil.close(in);
            IOUtil.close(out);
        }
    }

    protected File gzipIfRequested(File file) throws Exception {
        if (!gzip || (file == null) || (!file.exists())) {
            return null;
        }
        if (".gz".equalsIgnoreCase(FileUtils.getExtension(file.getName()))) {
            return null;
        }
        File gzipped = new File(file.getAbsolutePath() + ".gz");
        getLog().info(String.format("create gzip version : %s", gzipped.getName()));
        GZIPOutputStream out = null;
        FileInputStream in = null;
        try {
            out = new GZIPOutputStream(buildContext.newFileOutputStream(gzipped));
            in = new FileInputStream(file);
            IOUtil.copy(in, out);
        } finally {
            IOUtil.close(in);
            IOUtil.close(out);
        }
        return gzipped;
    }

    protected long ratioOfSize(File file100, File fileX) throws Exception {
        long v100 = Math.max(file100.length(), 1);
        long vX = Math.max(fileX.length(), 1);
        return (vX * 100)/v100;
    }
}
